Требования: закрыть все приложения от прямого доступа за reverse proxy сервисом в целях безопасности и возможности иметь единую точку входа, что позволит конфигурировать права доступа в едином механизме без необходимости внесения изменений в каждый сервис отдельно. Также это решение покрывает требования от ИБ
Основная задача
прокси в том, что он должен исходя из своей конфигурации позволять
обращаться к сервисам как с токеном авторизации, так и без него (для
возможности открыть страницу авторизации). Будет закрыт прямой доступ ко
всем существующим микросервисам и все запросы извне будут проходить
через proxy-gateway. После обсуждения с Сергеем Жемотелем принято
решение что закрытие всех микросервисов будет произведено путем удаления
их ingress, в итоге в кубере будет единственный ingress -
proxy-gateway.
Микросервис proxy-gateway разрабатывается на
SpringFramework с использованием spring-cloud-gateway. Это известный и
хорошо работающий reverse proxy сервис. Обсуждалось также использовать
service discovery (eureka), убрать все ingress из кубера, настроить все
сервисы на регистрацию в service discovery, тогда балансировкой будет
полноценно заниматься zuul proxy, но необходимости в этом нет. Поэтому
будем работать без service discovery, балансировка и circuit breaker
останется на плечах Service кубера.
Полезные для нас возможности spring-cloud-gateway:
- proxy- gateway в в этом случае будет написан на java и не будет необходимости писать новую логику для получения ролей токена, сейчас у нас есть библиотека от систематики - secure-configuration, которая всем этим занимается и имеет возможность подключения к spring приложению, в нее уже внесены доработки для поддержки webflux (требуется в spring-cloud-gateway).
- мы можем строить таблицу маршрутизации по любым доступным правилам - хост, path, хедеры, контент запроса. Так можно будет держать один proxy-gateway который будет работать например и с uat и с prod и с dev окружением, исходя из хоста запроса можно направлять траффик на нужный стенд.
- имеется возможность проксировать ws соединения, что будет необходимо для работы push уведомлениями в браузере.
Схема работы:
Ingress будет иметь только proxy-gateway, а все наши текущие сервисы будут доступны только через Service, таким образом можно будет убрать у них авторизацию т.к. доступ к ним будет возможен только через proxy-gateway который и будет проверять токены и роли пользователей. Внутри сети микросервисы должны работать через Service.
proxy- gateway требуется информация, по которой он может определять может ли он одобрить запрос к конкретному ендпоинту с той ролью, которая пришла в токене от клиента в виде "/path" - ["роли"]. Таблица маршрутизации хранится в базе Postgres - enpoints_access_info. Proxy-gateway получает содержимое таблицы маршрутов при запуске и далее каждые 10 минут (в текущей реализации пока так).
При таком подходе прокси будет иметь на руках информацию о доступах к ендпоинтам по ролям, далее нужно реализовать механизм актуализации этих данных, например при появлении новых ендпоинтов или при создании нового сервиса необходимо сразу же доставить эту информацию до прокси. И здесь проблема в человеческом факторе - сложно будет избежать опечаток или того что кто-то вообще забудет добавить эту информацию в базу. Поэтому сделано следующее: разработана библиотека pgs-proxy-library, которая содержит аннотацию по аналогии с аннотацией Secured будет принимать набор ролей в параметре, этой аннотацией необходимо будет разметить все ендпоинты сервиса, к которым нужен доступ из прокси, при старте приложения библиотека собирает всю информацию и отправляет ее в enpoints-access-info-service который будет сохранять ее в базе enpoints_access_info. Таким образом имеется два варианта заполнения таблицы маршрутизации:
- с помощью библиотеки pgs-proxy-library (micronaut 3, java 17). Полностью автоматический способ.
- в ручную заполнять таблицу маршрутизации.
Формат таблицы access_info:
имя поля | формат | описание | пример |
|---|---|---|---|
имя поля | формат | описание | пример |
| id | uuid | primary key | aefe7255-6111-4365-886a-e84d805d7b05 |
| app_name | varchar(255) | Имя приложения, должно быть равно имени его Service кубера | mz-history-2 |
| pkg | varchar(500) | Абсолютный путь к java классу с контроллером, поле для информации, не участвует в логике. | sx.microservices.mz.history.controller.RequestController |
| method | varchar(255) | метод запроса - GET, POST и т.п. | GET |
| ingress_path | varchar(500) | Базовый путь к приложению, то же самое что и сейчас у него в пути ingress. | /mz/history |
| path | varchar(500) | Путь к ендпоинту внутри приложения, например "/requests/example". Поддерживается wildcard, пример - "/request/*" - будет принимать любое значение на месте * Еще пример "/request/*/*" | /request/example |
| rules | jsonb | Json с полями perms и roles, где perms - массив permissions, roles - массив ролей. Можно указать либо что-то одно либо ничего вообще. | {"perms": null, "roles": null} - означает что для доступа достаточно лишь валидного токена, без разницы какие у него роли. |
| manual | boolean | Поле для определения в ручную заполнен этот маршрут запросом в базу или автоматически сервисом enpoints-access-info-service | false |
| modified | timestamp | Дата редактирования | 1682606850000 |
| modified_by | varchar(255) | Источник редактирования | Manual или AccessInfoService |
Принцип работы
При запуске proxy-gateway (далее - прокси) загружает таблицу маршрутизации из postgres (далее он ее актуализирует каждые 10 минут) и кладет к себе в кеш. При поступлении запроса срабатывает фильтр авторизации - из хедера Authorization либо из Cookie берется токен авторизации и производится проверка токена и извлечение его ролей из sso-service - это все стандартный механизм из secure-configuration, который сейчас используется на ПГС во всех сервисах где есть авторизация, затем он ищет среди ingress_path подходящий сервис для проксирования запроса, далее он определяет по path и method какие необходимы роли для доступа и если пришедший токен удовлетворяет условиям обращения к ендпоинту - проксирует запрос, иначе - выдает ошибку 401. Если не найден подходящий маршрут - возвращается ошибка 404. В прокси предусмотрены конфиги для безусловного доступа к ендпоинтам в не зависимости от наличия токена в запросе - whitelist paths, это было сделано строго для dev и uat площадок, сейчас там содержатся пути к swagger-ui и его составляющим, на проде не рекомендуется делать также. Для прокси дополнен параметр trustedIssuers - в него добавлено поле internalUrl - внутренний адрес sso-service (http://sso-service) для того, чтобы не ходить в sso (за проверкой токена) через внешнюю сеть (по факту через самого себя).
proxy_gateway: # для прода указать "prod" spring_profile: dev microws_uri: http://microws # внутренний путь к microws, будет нужен если будем прятать microws за прокси sso_service_uri: http://sso-service # внутренний адрес sso-service, нужен для проксирования запросов к sso-service no_roles_access_allowed: true # если true - то позволять обращаться к ендпоинтам у которых в таблице маршрутизации не заполнено поле rules - по факту это доступ к ендпоинту только с валидным токеном без проверки какие в нем есть роли # для прода white_list_paths оставить пустым, этот параметр позволяет заходить по перечисленным в нем путям без авторизации white_list_paths: /swagger-ui,/api,/res/swagger-ui-bundle.js,/res/swagger-ui-standalone-preset.js,/res/swagger-ui.css,/res/favicon-32x32.png,/res/favicon-16x16.png,/swagger/swagger.yml,/readiness,/liveness,/health max_request_size: 100MB |
Библиотека pgs-proxy-library на старте приложения находит все контроллеры с аннотацией PgsAccessRules, принимает из нее роли и permissions, формирует запись вида Method, Path, rules и отправляет через кафку в сервис endpoints-access-info, который сохраняет полную информацию о маршрутах в базу postgres актуализируя записи (если для этого сервиса записи уже есть - он их пересоздаст, таким образом поддерживается удаление и обновление ендпоинтов в таблице маршрутизации), при этом если у существующей записи стоит признак manual = true - то запись не будет обновлена либо удалена, это сделано для того, чтобы иметь возможность в ручную изменять записи и чтобы они после этого не перезаписывались автоматически.
Этапы реализации:

Примеры скриптов в таблицу access_info БД endpoints-access-info
Примеры указаны без учёта требования отдела DBA по оформлению скриптов для релиза.
Полностью одинаковых (дублей) записей в таблице access_info делать не нужно.
Добавление новой записи:
В первую очередь рекомендуется проверить есть ли уже такая запись, чтобы не плодить дубли. Если такая запись уже есть - проверить актуальные ли права доступа в ней.
INSERT INTO access_info(app_name,pkg,method,ingress_path,path,rules,manual) VALUES('ervu-application-gateway','Manual','Get','/service/ervu-application-gateway','/applicationInfo/getWithActive/*','{"perms": [], "roles": []}','true') |
Также есть примеры в комментарии к задачам и
где
app_name - имя приложения, должен быть равен строго имени сущности Service кубера, по нему будет строится путь к сервису, например если app_name = cnsi-service, то путь будет начинаться с http://cnsi-service
pkg - имя класса контроллера, необязательно, делалось для отладок, сейчас особо не используется
method - тип rest метода (get, post, path, put ...) регистронезависимый
ingress_path - внешний путь к сервису, сюда нужно указать то, что было указано в ingress сервиса, например /service/cnsi/service
path - путь к ендпоинту, относительно ingress, например если app_name = cnsi-service, ingress_path = /service/cnsi/service, path = /classifiers/reload - то полный путь будет сформирован так: http://cnsi-service/service/cnsi/service/classifiers/reload
rules - jsonb с правами доступа, внутри объекта должны лежать roles и perms, где roles - требуемые для доступа роли, perms - требуемые permissions. Необязательное поле, если не заполнено - то для доступа к ендпоинту потребуется только валидный токен
manual - если эта запись в таблицу добавляется в ручную - значение этого поля должно быть строго равно true
Примечение:
Изначально
обсуждалось что мы не будем использовать permissions, только roles.
Желательно этого придерживаться, в текущих записях все таки где-то
используем permissions.
Если есть возможность внедрить в сервис библиотеку pgs-proxy-library (http://nexus.gosuslugi.local/#nexus-search;quick~pgs-proxy-library) - то лучше использовать ее, она сама инициирует заполнение таблицы путей (через сообщения кафки в сервис endpoints-access-info) и будет их актуализировать. Основная версия для java17 и micronaut3, есть версии для java11 и micronaut2. Для работы библиотеки необходимо:
1. Обеспечить права на запись в топик pgs.proxy.endpoints.access.info
2. В application.yml добавить:
pgs-proxy:
enabled: true # на стендах пгс прокси не используется, поэтому там нужно выставить false
ingress-path: /service/ervu-object-diff-calc # сюда нужно указать
базовый путь к сервису, если был ingress - взять путь из него
packages: rtl.pgs.ervu.objectdiffcalc # путь в корневому package приложения
kafka:
endpoints-access-info-topic: pgs.proxy.endpoints.access.info # имя топика, в который нужно отправлять инфу об эндпоинтах сервиса, не менять
3. Убрать ingress из хелмов
4. Все методы, к которым необходим внешний доступ, обозначить аннотацией @PgsAccessRules, в параметрах этой аннотации указать roles и perms, либо не указывать (если нужен доступ просто по валидному токену)
После добавления записи в таблицу (либо после деплоя сервиса с включенной библиотекой pgs-proxy-library) сервис прокси подхватит изменения в течении 10 минут.
Обновление записи в таблице:
Обновление по id, задание прав доступа:
UPDATE access_info SET rules = '{"roles": ["Администратор облака"], "perms": "read_user"}'WHERE id = 2000000000001 |
Удаление записи в таблице:
Для удаления лучше всего использовать id чтобы не удалить лишнего
DELETEFROM access_infoWHERE id = <ID> |
Добавить комментарий